import numpy as np
import pandas as pd
import dalex as dx
import pickle
import matplotlib.pyplot as plt
import seaborn as sns
housing = pd.read_csv('housing_preprocessed.csv')
predictors = housing.drop('median_house_value', axis = 1)
target = housing.loc[:, ['median_house_value']]
xgb = pickle.load(open("xgb.pickle", 'rb'))
svr = pickle.load(open("svr.pickle", 'rb'))
mlp = pickle.load(open("mlp.pickle", 'rb'))
E:\anaconda\lib\site-packages\sklearn\base.py:310: UserWarning: Trying to unpickle estimator LinearSVR from version 0.23.2 when using version 0.24.1. This might lead to breaking code or invalid results. Use at your own risk. warnings.warn( E:\anaconda\lib\site-packages\sklearn\base.py:310: UserWarning: Trying to unpickle estimator MLPRegressor from version 0.23.2 when using version 0.24.1. This might lead to breaking code or invalid results. Use at your own risk. warnings.warn(
housing.head()
| longitude | latitude | housing_median_age | total_bedrooms | population | households | median_income | median_house_value | 1H OCEAN | INLAND | NEAR BAY | NEAR OCEAN | rooms_per_household | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | -122.23 | 37.88 | 41.0 | 129.0 | 322.0 | 126.0 | 8.3252 | 452600.0 | 0.0 | 0.0 | 1.0 | 0.0 | 6.984127 |
| 1 | -122.22 | 37.86 | 21.0 | 1106.0 | 2401.0 | 1138.0 | 8.3014 | 358500.0 | 0.0 | 0.0 | 1.0 | 0.0 | 6.238137 |
| 2 | -122.24 | 37.85 | 52.0 | 190.0 | 496.0 | 177.0 | 7.2574 | 352100.0 | 0.0 | 0.0 | 1.0 | 0.0 | 8.288136 |
| 3 | -122.25 | 37.85 | 52.0 | 235.0 | 558.0 | 219.0 | 5.6431 | 341300.0 | 0.0 | 0.0 | 1.0 | 0.0 | 5.817352 |
| 4 | -122.25 | 37.85 | 52.0 | 280.0 | 565.0 | 259.0 | 3.8462 | 342200.0 | 0.0 | 0.0 | 1.0 | 0.0 | 6.281853 |
Bazując na wynikach poprzedniej pracy domowej analizie poddałem zmienne latitude, longitude, median_income, households oraz population.
expl_xgb = dx.Explainer(xgb, predictors, target,
label = "XGBoost", verbose = False)
expl_svr = dx.Explainer(svr, predictors, target,
label = "Support Vector Machine", verbose = False)
expl_mlp = dx.Explainer(mlp, predictors, target,
label = "Multilayer Perceptron", verbose = False)
pdp_xgb = expl_xgb.model_profile()
pdp_svr = expl_svr.model_profile()
pdp_mlp = expl_mlp.model_profile()
Calculating ceteris paribus: 100%|█████████████████████████████████████████████████████| 12/12 [00:00<00:00, 21.56it/s] Calculating ceteris paribus: 100%|████████████████████████████████████████████████████| 12/12 [00:00<00:00, 108.18it/s] Calculating ceteris paribus: 100%|█████████████████████████████████████████████████████| 12/12 [00:00<00:00, 77.47it/s]
pdp_xgb.plot([pdp_svr, pdp_mlp],
variables = ['households', 'population'])
Widzimy, że model SVR oraz sieć neuronowa zachowują się problematycznie dla zmiennych households (zbyt duże predykcje dla większych - a więc mniej typowych - wartości) oraz population (SVR zwraca wyniki bez sensu dla większych obserwacji).
Przypomnijmy sobie rozkład tych zmiennych:
fg, axes = plt.subplots(1, 2)
sns.histplot(ax = axes[0],
x = 'population',
data = housing)
sns.histplot(ax = axes[1],
x = 'households',
data = housing)
plt.show()
Rozkład jest mocno skośny. Zobaczmy, jak będzie działać na danych bez outlierów.
predictors_with_cut_population = predictors.loc[predictors['population'] < np.quantile(predictors['population'], 0.95)]
target_with_cut_population = target[predictors['population'] < np.quantile(predictors['population'], 0.95)]
predictors_with_cut_households = predictors.loc[predictors['households'] < np.quantile(predictors['households'], 0.95)]
target_with_cut_households = target[predictors['households'] < np.quantile(predictors['households'], 0.95)]
expl_xgb_cut_pop = dx.Explainer(xgb, predictors_with_cut_population,
target_with_cut_population,
label = "XGBoost", verbose = False)
expl_svr_cut_pop = dx.Explainer(svr, predictors_with_cut_population,
target_with_cut_population,
label = "Support Vector Machine", verbose = False)
expl_mlp_cut_pop = dx.Explainer(mlp, predictors_with_cut_population,
target_with_cut_population,
label = "Multilayer Perceptron", verbose = False)
expl_xgb_cut_hh = dx.Explainer(xgb, predictors_with_cut_households,
target_with_cut_households,
label = "XGBoost", verbose = False)
expl_svr_cut_hh = dx.Explainer(svr, predictors_with_cut_households,
target_with_cut_households,
label = "Support Vector Machine", verbose = False)
expl_mlp_cut_hh = dx.Explainer(mlp, predictors_with_cut_households,
target_with_cut_households,
label = "Multilayer Perceptron", verbose = False)
pdp_xgb_cut_pop = expl_xgb_cut_pop.model_profile()
pdp_svr_cut_pop = expl_svr_cut_pop.model_profile()
pdp_mlp_cut_pop = expl_mlp_cut_pop.model_profile()
pdp_xgb_cut_hh = expl_xgb_cut_hh.model_profile()
pdp_svr_cut_hh = expl_svr_cut_hh.model_profile()
pdp_mlp_cut_hh = expl_mlp_cut_hh.model_profile()
Calculating ceteris paribus: 100%|█████████████████████████████████████████████████████| 12/12 [00:00<00:00, 20.74it/s] Calculating ceteris paribus: 100%|████████████████████████████████████████████████████| 12/12 [00:00<00:00, 113.17it/s] Calculating ceteris paribus: 100%|█████████████████████████████████████████████████████| 12/12 [00:00<00:00, 77.47it/s] Calculating ceteris paribus: 100%|█████████████████████████████████████████████████████| 12/12 [00:00<00:00, 22.28it/s] Calculating ceteris paribus: 100%|████████████████████████████████████████████████████| 12/12 [00:00<00:00, 113.28it/s] Calculating ceteris paribus: 100%|█████████████████████████████████████████████████████| 12/12 [00:00<00:00, 79.00it/s]
pdp_xgb_cut_pop.plot([pdp_svr_cut_pop, pdp_mlp_cut_pop],
variables = ['population'])
pdp_xgb_cut_hh.plot([pdp_svr_cut_hh, pdp_mlp_cut_hh],
variables = ['households'])
Tutaj już modele są nieco bardziej zgodne i sensowne. Dla obu zmiennych widzimy identyczne trendy (zwłaszcza dla mniejszych wartości, a więc najczęstszych). Trendy te zgadzają się z ogólną intuicją (większa populacja -> mniejsza wartość, więcej posiadłości -> bliżej centrum miasta -> większa wartość).
Spójrzmy na zmienne latitude oraz longitude.
pdp_xgb.plot([pdp_svr, pdp_mlp],
variables = ['latitude', 'longitude'])
Obserwujemy znaczne różnice między modelami. Jak było wspomniane w poprzedniej pracy domowej, model SVR oraz sieć neuronowa nie uznaje zmiennych latitude oraz longitude za istotne.
Ciekawe zależności możemy odczytać z krzywych dla modelu XGBoost - preferowane są nieruchomości bardziej na zachód (mniejsza długość geograficzna longitude) oraz na południe (mniejsza szerokość geograficzna latitude).
Ten pierwszy wynik jest zgodny z intuicją - bardziej na zachód jest bliżej do oceanu, tam są położone duże aglomeracje. Obserwujemy duży spadek dla wartości koło 122$^o$20'W (okolice San Francisco), potem długo nic, potem powolny spadek dla wartości 118$^o$20'W do 117$^o$20'W (na tych długościach jest Los Angeles oraz San Diego).
Dla szerokości ponownie spore znaczenie ma położenie wspomnianych miast - obserwujemy spadek między 33$^o$30'N a 34$^o$30'N (San Diego, Los Angeles) oraz ogromny spadek między 37$^o$30'N a 38$^o$N (San Fransisco).
Rzućmy jeszcze okiem na zmienną median_income.
pdp_xgb.plot([pdp_svr, pdp_mlp],
variables = ['median_income'])
Obserwujemy różnicę między modelami od pewnego momentu. Co warto zauważyć, predykcje modelu SVR oraz sieci neuronowej dla dużych wartości dochodów przestają mieć sens. Ponownie jest to pokłosie skośności rozkładu.
sns.histplot(x = 'median_income', data = housing)
<AxesSubplot:xlabel='median_income', ylabel='Count'>
Dla zdecydowanej większości obserwacji odpowiedzi modelu są zgodne.
Porównamy profile PDP oraz ALE dla zmiennych latitude i longitude dla modelu XGBoost.
ale_xgb = expl_xgb.model_profile(type = 'accumulated')
ale_svr = expl_svr.model_profile(type = 'accumulated')
ale_mlp = expl_mlp.model_profile(type = 'accumulated')
Calculating ceteris paribus: 100%|█████████████████████████████████████████████████████| 12/12 [00:00<00:00, 19.98it/s] Calculating accumulated dependency: 100%|██████████████████████████████████████████████| 12/12 [00:01<00:00, 6.70it/s] Calculating ceteris paribus: 100%|████████████████████████████████████████████████████| 12/12 [00:00<00:00, 110.16it/s] Calculating accumulated dependency: 100%|██████████████████████████████████████████████| 12/12 [00:01<00:00, 7.25it/s] Calculating ceteris paribus: 100%|█████████████████████████████████████████████████████| 12/12 [00:00<00:00, 74.58it/s] Calculating accumulated dependency: 100%|██████████████████████████████████████████████| 12/12 [00:01<00:00, 6.85it/s]
ale_xgb.result['_label_'] = 'ALE'
pdp_xgb.result['_label_'] = 'PDP'
ale_xgb.plot(pdp_xgb, variables=['latitude', 'longitude'])
Obserwujemy różnicę w wartościach, ale identyczny trend.
Spójrzmy na profile ALE dla zmiennej median_income.
ale_xgb.result['_label_'] = 'XGBoost'
ale_xgb.plot([ale_svr, ale_mlp], variables=['median_income'])
Ponownie wykresy różnią się od PDP o stałe.
Profile PDP to przydatne, czytelne narzędzie dostarczające informacji o wpływie zmiennej na odpowiedź modelu. Oprócz interpretowalnej informacji możemy łatwo wykryć, dla jakich wartości odpowiedź modelu staje się problematyczna. Dla przykładu w toku tej pracy domowej się okazało, że dla nielicznych, mocno odstających wartości model SVR oraz sieć neuronowa zwracały błędne wartości, ale dla wartości nieodstających ich odpowiedzi były sensowne.